import * as simpleGit from 'simple-git';
import * as moment from 'moment';

export interface Commit {
    hash: string;
    date: Date;
    message: string;
    author: string;
}

export interface LineStats {
    added: number;
    deleted: number;
    modified: number;
    total: number;
}

export interface GitSummaryStats {
    commits: Commit[];
    lineStats: LineStats;
    contributors: Set<string>;
}

export class GitService {
    async getCommits(repoPath: string): Promise<Commit[]> {
        const git = simpleGit.default(repoPath);
        const log = await git.log();
        
        return log.all.map(commit => ({
            hash: commit.hash,
            date: new Date(commit.date),
            message: commit.message,
            author: commit.author_name
        }));
    }
    
    async getCommitsInRange(repoPath: string, fromDate: string, toDate: string): Promise<Commit[]> {
        const git = simpleGit.default(repoPath);
        
        // Format date range for git log
        const fromDateFormatted = moment(fromDate).format('YYYY-MM-DD');
        const toDateFormatted = moment(toDate).add(1, 'days').format('YYYY-MM-DD');
        
        // Use the proper options format for simple-git
        const options = {
            '--after': `"${fromDateFormatted}"`,
            '--before': `"${toDateFormatted}"`,
            '--date': 'iso',
            '--pretty': 'format:%H|%ad|%an|%s',
            '--all': null,
            '--no-merges': null,
            '--date-order': null
        };
        
        const log = await git.log(options);
        
        return log.all.map(commit => ({
            hash: commit.hash,
            date: new Date(commit.date),
            message: commit.message,
            author: commit.author_name
        }));
    }

    async getGitSummaryStats(repoPath: string): Promise<GitSummaryStats> {
        const git = simpleGit.default(repoPath);
        const commits = await this.getCommits(repoPath);
        
        // Get line statistics
        const lineStats = await this.getLineStats(git, commits);
        
        // Get unique contributors
        const contributors = new Set<string>();
        commits.forEach(commit => contributors.add(commit.author));
        
        return {
            commits,
            lineStats,
            contributors
        };
    }
    
    async getGitSummaryStatsInRange(repoPath: string, fromDate: string, toDate: string): Promise<GitSummaryStats> {
        const git = simpleGit.default(repoPath);
        const commits = await this.getCommitsInRange(repoPath, fromDate, toDate);
        
        // Format date range for git diff
        const fromDateFormatted = moment(fromDate).format('YYYY-MM-DD');
        const toDateFormatted = moment(toDate).add(1, 'days').format('YYYY-MM-DD');
        
        // Get line statistics for the date range
        const lineStats = await this.getLineStatsInRange(git, commits, fromDateFormatted, toDateFormatted);
        
        // Get unique contributors
        const contributors = new Set<string>();
        commits.forEach(commit => contributors.add(commit.author));
        
        return {
            commits,
            lineStats,
            contributors
        };
    }
    
    private async getLineStats(git: simpleGit.SimpleGit, commits: Commit[]): Promise<LineStats> {
        try {
            // Get total line count - using a more reliable approach for Windows
            let totalLines = 0;
            try {
                // First try to get a list of all files tracked by git
                const filesResult = await git.raw(['ls-files']);
                const files = filesResult.trim().split('\n').filter(f => f.trim().length > 0);
                
                // Then count lines in each file
                let totalCount = 0;
                for (const file of files) {
                    try {
                        // Skip binary files and directories
                        const fileType = await git.raw(['check-attr', 'text', file]);
                        if (fileType.includes('text: unset') || fileType.includes('text: binary')) {
                            continue;
                        }
                        
                        // Count lines in the file
                        const wc = await git.raw(['cat-file', '-p', `HEAD:${file}`]);
                        const lineCount = wc.split('\n').length;
                        totalCount += lineCount;
                    } catch (err) {
                        // Skip files that can't be counted
                        console.log(`Skipping file ${file}: ${err}`);
                    }
                }
                totalLines = totalCount;
            } catch (err) {
                console.error('Error counting lines:', err);
                // Fallback to a simpler but less accurate method
                totalLines = 0;
            }
            
            // Get summary of changes by analyzing all commits
            let added = 0;
            let deleted = 0;
            let modified = 0;
            
            // If we have commits, get the first commit hash to use as base
            if (commits.length > 0) {
                // Sort commits by date (oldest first)
                const sortedCommits = [...commits].sort((a, b) => a.date.getTime() - b.date.getTime());
                
                // Get the stats for all commits
                try {
                    // Try to get the first commit
                    const firstCommitHash = sortedCommits[sortedCommits.length - 1].hash;
                    
                    // Get the root commit (first commit in the repository)
                    const rootCommitResult = await git.raw(['rev-list', '--max-parents=0', 'HEAD']);
                    const rootCommit = rootCommitResult.trim();
                    
                    // Get stats from the root commit to HEAD
                    const summaryResult = await git.raw(['diff', '--shortstat', `${rootCommit}..HEAD`]);
                    const stats = this.parseShortStat(summaryResult);
                    
                    added = stats.added;
                    deleted = stats.deleted;
                    modified = stats.modified;
                } catch (error) {
                    console.error('Error getting commit stats:', error);
                    
                    // Fallback: analyze each commit individually
                    for (const commit of sortedCommits) {
                        try {
                            const commitStats = await git.raw(['show', '--shortstat', '--pretty=format:', commit.hash]);
                            const stats = this.parseShortStat(commitStats);
                            added += stats.added;
                            deleted += stats.deleted;
                            modified += stats.modified;
                        } catch (err) {
                            console.log(`Skipping commit ${commit.hash}: ${err}`);
                        }
                    }
                }
            }
            
            return {
                added,
                deleted,
                modified,
                total: totalLines
            };
        } catch (error) {
            console.error('Error getting line stats:', error);
            return { added: 0, deleted: 0, modified: 0, total: 0 };
        }
    }
    
    private async getLineStatsInRange(git: simpleGit.SimpleGit, commits: Commit[], fromDate: string, toDate: string): Promise<LineStats> {
        try {
            // Get total line count using the same method as getLineStats
            let totalLines = 0;
            try {
                const filesResult = await git.raw(['ls-files']);
                const files = filesResult.trim().split('\n').filter(f => f.trim().length > 0);
                
                let totalCount = 0;
                for (const file of files) {
                    try {
                        const fileType = await git.raw(['check-attr', 'text', file]);
                        if (fileType.includes('text: unset') || fileType.includes('text: binary')) {
                            continue;
                        }
                        
                        const wc = await git.raw(['cat-file', '-p', `HEAD:${file}`]);
                        const lineCount = wc.split('\n').length;
                        totalCount += lineCount;
                    } catch (err) {
                        console.log(`Skipping file ${file}: ${err}`);
                    }
                }
                totalLines = totalCount;
            } catch (err) {
                console.error('Error counting lines:', err);
                totalLines = 0;
            }
            
            // Get summary of changes in date range by analyzing commits in that range
            let added = 0;
            let deleted = 0;
            let modified = 0;
            
            if (commits.length > 0) {
                // Try to get stats for the date range using git diff
                try {
                    const summaryResult = await git.raw([
                        'log', 
                        '--numstat', 
                        '--pretty=format:', 
                        `--since="${fromDate}"`, 
                        `--until="${toDate}"`
                    ]);
                    
                    // Parse numstat output
                    const lines = summaryResult.trim().split('\n').filter(line => line.trim().length > 0);
                    let filesChanged = new Set<string>();
                    
                    for (const line of lines) {
                        const parts = line.trim().split(/\s+/);
                        if (parts.length >= 3) {
                            const [addedStr, deletedStr, filename] = parts;
                            
                            // Skip binary files (shown as "-" in git numstat)
                            if (addedStr !== '-' && deletedStr !== '-') {
                                added += parseInt(addedStr, 10) || 0;
                                deleted += parseInt(deletedStr, 10) || 0;
                                filesChanged.add(filename);
                            }
                        }
                    }
                    
                    modified = filesChanged.size;
                } catch (error) {
                    console.error('Error getting stats in date range:', error);
                    
                    // Fallback: analyze each commit in the range individually
                    for (const commit of commits) {
                        try {
                            const commitStats = await git.raw(['show', '--shortstat', '--pretty=format:', commit.hash]);
                            const stats = this.parseShortStat(commitStats);
                            added += stats.added;
                            deleted += stats.deleted;
                            modified += stats.modified;
                        } catch (err) {
                            console.log(`Skipping commit ${commit.hash}: ${err}`);
                        }
                    }
                }
            }
            
            return {
                added,
                deleted,
                modified,
                total: totalLines
            };
        } catch (error) {
            console.error('Error getting line stats in range:', error);
            return { added: 0, deleted: 0, modified: 0, total: 0 };
        }
    }
    
    private parseShortStat(shortstat: string): { added: number, deleted: number, modified: number } {
        let added = 0;
        let deleted = 0;
        let modified = 0;
        
        if (!shortstat) {
            return { added, deleted, modified };
        }
        
        // Example: " 5 files changed, 10 insertions(+), 5 deletions(-)"
        const insertMatch = shortstat.match(/(\d+) insertion/);
        const deleteMatch = shortstat.match(/(\d+) deletion/);
        const fileMatch = shortstat.match(/(\d+) files? changed/);
        
        if (insertMatch) {
            added = parseInt(insertMatch[1], 10);
        }
        
        if (deleteMatch) {
            deleted = parseInt(deleteMatch[1], 10);
        }
        
        if (fileMatch) {
            modified = parseInt(fileMatch[1], 10);
        }
        
        return { added, deleted, modified };
    }
    
    private parseTotalLines(totalLinesOutput: string): number {
        try {
            // The output format is typically a list of line counts per file, with a total at the end
            // We'll extract just the total number
            const lines = totalLinesOutput.trim().split('\n');
            const lastLine = lines[lines.length - 1].trim();
            
            // Extract the first number from the last line, which should be the total
            const match = lastLine.match(/^\s*(\d+)/);
            if (match) {
                return parseInt(match[1], 10);
            }
            
            // If we couldn't parse it, add up all the numbers at the start of each line
            return lines.reduce((total, line) => {
                const match = line.match(/^\s*(\d+)/);
                if (match) {
                    return total + parseInt(match[1], 10);
                }
                return total;
            }, 0);
        } catch (error) {
            console.error('Error parsing total lines:', error);
            return 0;
        }
    }
}
